<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>图片转视频工具</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/ffmpeg/0.11.6/ffmpeg.min.js"></script>
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
        font-family: 'SF Pro Display', 'PingFang SC', 'Helvetica Neue', Arial,
          sans-serif;
      }

      :root {
        --primary-bg: #e6e7ee;
        --secondary-bg: #ffffff;
        --shadow-light: #ffffff;
        --shadow-dark: #d1d9e6;
        --accent-color: #667eea;
        --accent-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        --success-color: #4ecdc4;
        --danger-color: #ff6b6b;
        --warning-color: #feca57;
        --text-primary: #2d3748;
        --text-secondary: #718096;
        --glass-bg: rgba(255, 255, 255, 0.25);
        --glass-border: rgba(255, 255, 255, 0.18);
      }

      body {
        background: var(--primary-bg);
        color: var(--text-primary);
        min-height: 100vh;
        padding: 20px;
        display: flex;
        flex-direction: column;
        align-items: center;
        transition: all 0.3s ease;
        animation: pageLoad 0.8s ease;
      }

      @keyframes pageLoad {
        from {
          opacity: 0;
          transform: translateY(20px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .container {
        width: 100%;
        max-width: 1200px;
        background: var(--primary-bg);
        border-radius: 30px;
        padding: 40px;
        box-shadow: 20px 20px 60px var(--shadow-dark),
          -20px -20px 60px var(--shadow-light);
        margin-top: 20px;
        transition: all 0.3s ease;
        animation: containerFloat 1s ease 0.2s both;
      }

      @keyframes containerFloat {
        from {
          opacity: 0;
          transform: translateY(30px) scale(0.95);
        }
        to {
          opacity: 1;
          transform: translateY(0) scale(1);
        }
      }

      .container:hover {
        box-shadow: 25px 25px 70px var(--shadow-dark),
          -25px -25px 70px var(--shadow-light);
      }

      header {
        text-align: center;
        margin-bottom: 40px;
        padding: 30px;
        background: var(--primary-bg);
        border-radius: 25px;
        box-shadow: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
        animation: headerSlide 0.8s ease 0.4s both;
      }

      @keyframes headerSlide {
        from {
          opacity: 0;
          transform: translateY(-30px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      h1 {
        font-size: 3.2rem;
        margin-bottom: 15px;
        background: var(--accent-gradient);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
        font-weight: 700;
        animation: titleGlow 3s ease-in-out infinite alternate;
      }

      @keyframes titleGlow {
        from {
          filter: brightness(1);
        }
        to {
          filter: brightness(1.2);
        }
      }

      .subtitle {
        font-size: 1.3rem;
        color: var(--text-secondary);
        margin-bottom: 30px;
        font-weight: 500;
      }

      .panels {
        display: flex;
        gap: 30px;
        margin-bottom: 40px;
        animation: panelsSlide 0.8s ease 0.6s both;
      }

      @keyframes panelsSlide {
        from {
          opacity: 0;
          transform: translateX(-20px);
        }
        to {
          opacity: 1;
          transform: translateX(0);
        }
      }

      .panel {
        flex: 1;
        background: var(--primary-bg);
        border-radius: 25px;
        padding: 30px;
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
        transition: all 0.3s ease;
        position: relative;
        overflow: hidden;
      }

      .panel::before {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.1),
          transparent
        );
        transition: left 0.8s;
      }

      .panel:hover::before {
        left: 100%;
      }

      .panel:hover {
        transform: translateY(-5px);
        box-shadow: 16px 16px 32px var(--shadow-dark),
          -16px -16px 32px var(--shadow-light);
      }

      .panel-title {
        font-size: 1.6rem;
        margin-bottom: 25px;
        color: var(--accent-color);
        display: flex;
        align-items: center;
        font-weight: 600;
      }

      .panel-title::before {
        content: attr(data-icon);
        margin-right: 12px;
        font-size: 1.8rem;
        animation: iconBounce 2s ease-in-out infinite;
      }

      @keyframes iconBounce {
        0%,
        100% {
          transform: scale(1);
        }
        50% {
          transform: scale(1.1);
        }
      }

      .file-upload {
        border: 3px dashed var(--accent-color);
        border-radius: 25px;
        padding: 50px 40px;
        text-align: center;
        transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
        margin-bottom: 25px;
        cursor: pointer;
        background: var(--primary-bg);
        box-shadow: inset 8px 8px 16px var(--shadow-dark),
          inset -8px -8px 16px var(--shadow-light);
        position: relative;
        overflow: hidden;
      }

      .file-upload:hover {
        border-color: var(--success-color);
        transform: translateY(-5px) scale(1.02);
        box-shadow: inset 10px 10px 20px var(--shadow-dark),
          inset -10px -10px 20px var(--shadow-light),
          0 15px 40px rgba(102, 126, 234, 0.25), 0 5px 15px rgba(0, 0, 0, 0.1);
      }

      .file-upload.dragover {
        border-color: var(--success-color);
        background: rgba(78, 205, 196, 0.05);
        transform: scale(1.05);
        box-shadow: inset 12px 12px 24px var(--shadow-dark),
          inset -12px -12px 24px var(--shadow-light),
          0 20px 50px rgba(78, 205, 196, 0.3);
        animation: dragPulse 1s ease-in-out infinite;
      }

      @keyframes dragPulse {
        0%,
        100% {
          border-color: var(--success-color);
        }
        50% {
          border-color: var(--accent-color);
        }
      }

      .file-upload-content {
        position: relative;
        z-index: 1;
      }

      .file-upload-icon {
        font-size: 3.5rem;
        margin-bottom: 20px;
        display: block;
        animation: iconFloat 3s ease-in-out infinite;
        filter: drop-shadow(0 4px 8px rgba(102, 126, 234, 0.2));
      }

      @keyframes iconFloat {
        0%,
        100% {
          transform: translateY(0);
        }
        50% {
          transform: translateY(-10px);
        }
      }

      .file-upload-text {
        font-size: 1.4rem;
        font-weight: 600;
        color: var(--text-primary);
        margin-bottom: 10px;
        background: var(--accent-gradient);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
      }

      .file-upload-subtext {
        font-size: 1.1rem;
        color: var(--text-secondary);
        margin-bottom: 20px;
        font-weight: 500;
      }

      .file-upload-formats {
        font-size: 0.9rem;
        color: var(--accent-color);
        font-weight: 500;
        padding: 8px 16px;
        background: rgba(102, 126, 234, 0.1);
        border-radius: 20px;
        display: inline-block;
        margin-bottom: 20px;
      }

      .file-upload input[type='file'] {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        opacity: 0;
        cursor: pointer;
        z-index: 2;
      }

      .image-preview {
        width: 100%;
        background: var(--primary-bg);
        border-radius: 20px;
        overflow: hidden;
        margin-top: 20px;
        box-shadow: inset 10px 10px 20px var(--shadow-dark),
          inset -10px -10px 20px var(--shadow-light);
        transition: all 0.3s ease;
      }

      .image-preview:hover {
        box-shadow: inset 12px 12px 24px var(--shadow-dark),
          inset -12px -12px 24px var(--shadow-light);
      }

      .image-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
        gap: 15px;
        padding: 20px;
        max-height: 300px;
        overflow-y: auto;
      }

      .image-item {
        position: relative;
        border-radius: 15px;
        overflow: hidden;
        box-shadow: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
        transition: all 0.3s ease;
      }

      .image-item:hover {
        transform: scale(1.05);
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
      }

      .image-item img {
        width: 100%;
        height: 80px;
        object-fit: cover;
        display: block;
      }

      .image-item .remove-btn {
        position: absolute;
        top: 5px;
        right: 5px;
        background: var(--danger-color);
        color: white;
        border: none;
        border-radius: 50%;
        width: 24px;
        height: 24px;
        font-size: 12px;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        opacity: 0;
        transition: all 0.3s ease;
      }

      .image-item:hover .remove-btn {
        opacity: 1;
      }

      .image-item .order-number {
        position: absolute;
        bottom: 5px;
        left: 5px;
        background: var(--accent-color);
        color: white;
        border-radius: 50%;
        width: 20px;
        height: 20px;
        font-size: 10px;
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: bold;
      }

      audio {
        width: 100%;
        margin-top: 10px;
      }

      button {
        background: var(--primary-bg);
        color: var(--text-primary);
        border: none;
        padding: 14px 28px;
        border-radius: 15px;
        cursor: pointer;
        font-weight: 600;
        transition: all 0.3s ease;
        font-size: 1rem;
        flex-shrink: 0;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
        position: relative;
        overflow: hidden;
      }

      button::before {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.2),
          transparent
        );
        transition: left 0.5s;
      }

      button:hover::before {
        left: 100%;
      }

      button:hover {
        transform: translateY(-3px);
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
      }

      button:active {
        transform: translateY(0);
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
      }

      button:disabled {
        opacity: 0.6;
        cursor: not-allowed;
        transform: none;
        box-shadow: 4px 4px 8px var(--shadow-dark),
          -4px -4px 8px var(--shadow-light);
      }

      .btn-primary {
        background: var(--accent-gradient);
        color: white;
        padding: 16px 32px;
        font-size: 1.1rem;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light),
          inset 0 1px 0 rgba(255, 255, 255, 0.2);
        margin: 20px auto;
        display: block;
      }

      .btn-primary:hover {
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light),
          inset 0 1px 0 rgba(255, 255, 255, 0.2),
          0 0 30px rgba(102, 126, 234, 0.3);
      }

      .progress-container {
        margin: 30px 0;
        animation: progressSlide 0.8s ease 0.8s both;
        display: none;
      }

      @keyframes progressSlide {
        from {
          opacity: 0;
          transform: translateY(20px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .progress-bar {
        height: 16px;
        background: var(--primary-bg);
        border-radius: 20px;
        overflow: hidden;
        margin-bottom: 15px;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
        position: relative;
      }

      .progress-fill {
        height: 100%;
        background: var(--accent-gradient);
        width: 0%;
        transition: width 0.3s ease;
        border-radius: 20px;
        box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
        position: relative;
        overflow: hidden;
      }

      .progress-fill::after {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.3),
          transparent
        );
        animation: progressShine 2s infinite;
      }

      @keyframes progressShine {
        0% {
          left: -100%;
        }
        100% {
          left: 100%;
        }
      }

      .progress-text {
        text-align: center;
        font-size: 1rem;
        color: var(--text-secondary);
        font-weight: 500;
      }

      .status-message {
        min-height: 35px;
        margin: 20px 0;
        color: var(--accent-color);
        font-weight: 600;
        font-size: 1.1rem;
        transition: all 0.3s ease;
        text-align: center;
      }

      .status-message.processing {
        animation: statusPulse 1.5s ease-in-out infinite;
      }

      @keyframes statusPulse {
        0%,
        100% {
          opacity: 1;
        }
        50% {
          opacity: 0.6;
        }
      }

      .error-message {
        color: var(--danger-color);
        margin: 20px 0;
        min-height: 35px;
        font-weight: 600;
        font-size: 1.1rem;
        animation: errorShake 0.5s ease;
        text-align: center;
      }

      @keyframes errorShake {
        0%,
        100% {
          transform: translateX(0);
        }
        25% {
          transform: translateX(-5px);
        }
        75% {
          transform: translateX(5px);
        }
      }

      .result-section {
        display: none;
        text-align: center;
        margin-top: 30px;
        padding: 25px;
        background: var(--primary-bg);
        border-radius: 25px;
        box-shadow: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
      }

      .result-video {
        width: 100%;
        max-width: 600px;
        margin: 20px auto;
        border-radius: 15px;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
      }

      .download-btn {
        background: linear-gradient(
          135deg,
          var(--success-color) 0%,
          #44a08d 100%
        );
        color: white;
        padding: 16px 32px;
        font-size: 1.1rem;
        margin: 20px 10px;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light),
          inset 0 1px 0 rgba(255, 255, 255, 0.2);
      }

      .file-info {
        margin-top: 15px;
        padding: 15px;
        background: rgba(102, 126, 234, 0.1);
        border-radius: 15px;
        font-size: 0.9rem;
        color: var(--text-secondary);
      }

      .instructions {
        background: var(--primary-bg);
        border-radius: 25px;
        padding: 30px;
        margin-top: 40px;
        box-shadow: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
        animation: instructionsSlide 0.8s ease 1.4s both;
      }

      @keyframes instructionsSlide {
        from {
          opacity: 0;
          transform: translateY(30px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .instructions h3 {
        color: var(--accent-color);
        margin-bottom: 20px;
        font-size: 1.4rem;
        font-weight: 600;
      }

      .instructions ol {
        padding-left: 25px;
        margin-bottom: 20px;
      }

      .instructions li {
        margin-bottom: 12px;
        line-height: 1.7;
        color: var(--text-secondary);
        font-weight: 500;
      }

      .instructions p {
        color: var(--text-secondary);
        font-style: italic;
        font-weight: 500;
        padding: 15px;
        background: rgba(102, 126, 234, 0.1);
        border-radius: 15px;
        border-left: 4px solid var(--accent-color);
      }

      footer {
        margin-top: 40px;
        text-align: center;
        color: var(--text-secondary);
        font-size: 0.95rem;
        padding: 25px;
        background: var(--primary-bg);
        border-radius: 20px;
        box-shadow: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
        animation: footerFade 0.8s ease 1.6s both;
      }

      @keyframes footerFade {
        from {
          opacity: 0;
        }
        to {
          opacity: 1;
        }
      }

      /* 设置选项样式 */
      .settings-section {
        background: var(--primary-bg);
        border-radius: 25px;
        padding: 30px;
        margin: 30px 0;
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
        animation: settingsSlide 0.8s ease 1s both;
      }

      @keyframes settingsSlide {
        from {
          opacity: 0;
          transform: translateY(20px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .settings-title {
        font-size: 1.6rem;
        color: var(--accent-color);
        margin-bottom: 25px;
        text-align: center;
        font-weight: 600;
      }

      .settings-grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
        gap: 25px;
        margin-bottom: 25px;
      }

      .setting-group {
        background: var(--primary-bg);
        border-radius: 20px;
        padding: 20px;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
      }

      .setting-group h4 {
        color: var(--accent-color);
        margin-bottom: 15px;
        font-size: 1.2rem;
        font-weight: 600;
      }

      .control-group {
        margin-bottom: 15px;
      }

      .control-group label {
        display: block;
        font-size: 1rem;
        color: var(--text-primary);
        margin-bottom: 8px;
        font-weight: 500;
      }

      .control-group input[type='number'],
      .control-group select {
        width: 100%;
        padding: 12px 16px;
        border: none;
        border-radius: 15px;
        background: var(--primary-bg);
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
        font-size: 1rem;
        color: var(--text-primary);
        transition: all 0.3s ease;
      }

      .control-group input[type='number']:focus,
      .control-group select:focus {
        outline: none;
        box-shadow: inset 8px 8px 16px var(--shadow-dark),
          inset -8px -8px 16px var(--shadow-light),
          0 0 0 2px var(--accent-color);
      }

      .filter-options {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
        gap: 10px;
        margin-top: 10px;
      }

      .filter-option {
        position: relative;
      }

      .filter-option input[type='radio'] {
        position: absolute;
        opacity: 0;
        width: 0;
        height: 0;
      }

      .filter-label {
        display: block;
        padding: 10px;
        background: var(--primary-bg);
        border-radius: 15px;
        cursor: pointer;
        transition: all 0.3s ease;
        box-shadow: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
        text-align: center;
        font-size: 0.9rem;
        font-weight: 500;
        border: 2px solid transparent;
      }

      .filter-label:hover {
        transform: translateY(-2px);
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
      }

      .filter-option input[type='radio']:checked + .filter-label {
        border-color: var(--accent-color);
        box-shadow: inset 4px 4px 8px var(--shadow-dark),
          inset -4px -4px 8px var(--shadow-light),
          0 0 15px rgba(102, 126, 234, 0.3);
      }

      .transition-options {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
        gap: 10px;
        margin-top: 10px;
      }

      .transition-option {
        position: relative;
      }

      .transition-option input[type='radio'] {
        position: absolute;
        opacity: 0;
        width: 0;
        height: 0;
      }

      .transition-label {
        display: block;
        padding: 10px;
        background: var(--primary-bg);
        border-radius: 15px;
        cursor: pointer;
        transition: all 0.3s ease;
        box-shadow: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
        text-align: center;
        font-size: 0.9rem;
        font-weight: 500;
        border: 2px solid transparent;
      }

      .transition-label:hover {
        transform: translateY(-2px);
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
      }

      .transition-option input[type='radio']:checked + .transition-label {
        border-color: var(--accent-color);
        box-shadow: inset 4px 4px 8px var(--shadow-dark),
          inset -4px -4px 8px var(--shadow-light),
          0 0 15px rgba(102, 126, 234, 0.3);
      }

      .note {
        font-size: 0.9rem;
        color: var(--text-secondary);
        font-style: italic;
        padding: 10px 15px;
        background: rgba(102, 126, 234, 0.1);
        border-radius: 10px;
        border-left: 3px solid var(--accent-color);
        margin-top: 15px;
      }

      @media (max-width: 768px) {
        .panels {
          flex-direction: column;
        }

        h1 {
          font-size: 2.5rem;
        }

        .container {
          padding: 20px;
        }

        .settings-grid {
          grid-template-columns: 1fr;
        }

        .filter-options {
          grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
        }

        .transition-options {
          grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
        }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <header>
        <h1>🎨 图片转视频工具</h1>
        <p class="subtitle">
          使用 FFmpeg.wasm 将图片转换为视频，支持多种效果和过渡动画
        </p>
      </header>

      <div class="panels">
        <!-- 图片上传面板 -->
        <div class="panel">
          <h3 class="panel-title" data-icon="🖼️">上传图片文件</h3>
          <div class="file-upload" id="imageUpload">
            <div class="file-upload-content">
              <span class="file-upload-icon">🎨</span>
              <div class="file-upload-text">点击或拖拽上传图片</div>
              <div class="file-upload-subtext">支持多张图片同时上传</div>
              <div class="file-upload-formats">JPG, PNG, WEBP, GIF, BMP</div>
            </div>
            <input type="file" id="imageInput" accept="image/*" multiple />
          </div>
          <div class="image-preview" id="imagePreview" style="display: none">
            <div class="image-grid" id="imageGrid"></div>
            <div class="file-info" id="imageInfo"></div>
          </div>
        </div>

        <!-- 音频上传面板 -->
        <div class="panel">
          <h3 class="panel-title" data-icon="🎵">上传音频文件</h3>
          <div class="file-upload" id="audioUpload">
            <div class="file-upload-content">
              <span class="file-upload-icon">🎵</span>
              <div class="file-upload-text">点击或拖拽上传音频</div>
              <div class="file-upload-subtext">视频时长将匹配音频时长</div>
              <div class="file-upload-formats">MP3, WAV, AAC, OGG, FLAC</div>
            </div>
            <input type="file" id="audioInput" accept="audio/*" />
          </div>
          <div class="image-preview" id="audioPreview" style="display: none">
            <audio id="audioElement" controls></audio>
            <div class="file-info" id="audioInfo"></div>
          </div>
        </div>
      </div>

      <!-- 设置选项区域 -->
      <div class="settings-section" id="settingsSection">
        <h3 class="settings-title">⚙️ 视频设置</h3>
        <div class="settings-grid">
          <!-- 基本设置 -->
          <div class="setting-group">
            <h4>📐 基本设置</h4>
            <div class="control-group">
              <label for="imageDuration">每张图片显示时长 (秒):</label>
              <input
                type="number"
                id="imageDuration"
                min="0.5"
                step="0.1"
                value="2"
              />
            </div>
            <div class="control-group">
              <label for="transitionDuration">过渡效果时长 (秒):</label>
              <input
                type="number"
                id="transitionDuration"
                min="0"
                step="0.1"
                value="0.5"
              />
            </div>
            <div class="control-group">
              <label for="videoSize">视频尺寸:</label>
              <select id="videoSize">
                <option value="1920x1080">1920x1080 (Full HD)</option>
                <option value="1280x720">1280x720 (HD)</option>
                <option value="854x480">854x480 (SD)</option>
                <option value="640x360">640x360 (小尺寸)</option>
              </select>
            </div>
          </div>

          <!-- 过渡效果 -->
          <div class="setting-group">
            <h4>🎬 过渡效果</h4>
            <div class="transition-options">
              <div class="transition-option">
                <input
                  type="radio"
                  id="transitionNone"
                  name="transition"
                  value="none"
                  checked
                />
                <label for="transitionNone" class="transition-label">无</label>
              </div>
              <div class="transition-option">
                <input
                  type="radio"
                  id="transitionFade"
                  name="transition"
                  value="fade"
                />
                <label for="transitionFade" class="transition-label"
                  >淡入淡出</label
                >
              </div>
              <div class="transition-option">
                <input
                  type="radio"
                  id="transitionSlide"
                  name="transition"
                  value="slide"
                />
                <label for="transitionSlide" class="transition-label"
                  >滑动</label
                >
              </div>
              <div class="transition-option">
                <input
                  type="radio"
                  id="transitionZoom"
                  name="transition"
                  value="zoom"
                />
                <label for="transitionZoom" class="transition-label"
                  >缩放</label
                >
              </div>
            </div>
            <div class="note">💡 多张图片时的切换效果</div>
          </div>

          <!-- 滤镜效果 -->
          <div class="setting-group">
            <h4>🎨 滤镜效果</h4>
            <div class="filter-options">
              <div class="filter-option">
                <input
                  type="radio"
                  id="filterNone"
                  name="filter"
                  value="none"
                  checked
                />
                <label for="filterNone" class="filter-label">无</label>
              </div>
              <div class="filter-option">
                <input
                  type="radio"
                  id="filterBlur"
                  name="filter"
                  value="blur"
                />
                <label for="filterBlur" class="filter-label">模糊</label>
              </div>
              <div class="filter-option">
                <input
                  type="radio"
                  id="filterSharpen"
                  name="filter"
                  value="sharpen"
                />
                <label for="filterSharpen" class="filter-label">锐化</label>
              </div>
              <div class="filter-option">
                <input
                  type="radio"
                  id="filterSepia"
                  name="filter"
                  value="sepia"
                />
                <label for="filterSepia" class="filter-label">复古</label>
              </div>
              <div class="filter-option">
                <input
                  type="radio"
                  id="filterGrayscale"
                  name="filter"
                  value="grayscale"
                />
                <label for="filterGrayscale" class="filter-label">黑白</label>
              </div>
              <div class="filter-option">
                <input
                  type="radio"
                  id="filterVintage"
                  name="filter"
                  value="vintage"
                />
                <label for="filterVintage" class="filter-label">怀旧</label>
              </div>
            </div>
            <div class="note">💡 应用于所有图片的视觉效果</div>
          </div>
        </div>
      </div>

      <!-- 处理按钮 -->
      <button class="btn-primary" id="processBtn" disabled>
        🚀 开始生成视频
      </button>

      <!-- 进度条 -->
      <div class="progress-container" id="progressContainer">
        <div class="progress-bar">
          <div class="progress-fill" id="progressFill"></div>
        </div>
        <div class="progress-text" id="progressText">准备中...</div>
      </div>

      <!-- 状态消息 -->
      <div class="status-message" id="statusMessage"></div>
      <div class="error-message" id="errorMessage"></div>

      <!-- 结果区域 -->
      <div class="result-section" id="resultSection">
        <h3>✅ 视频生成完成</h3>
        <video class="result-video" id="resultVideo" controls></video>
        <div>
          <button class="download-btn" id="downloadBtn">📥 下载视频</button>
          <button class="btn-primary" id="resetBtn">🔄 重新开始</button>
        </div>
      </div>

      <!-- 使用说明 -->
      <div class="instructions">
        <h3>📋 使用说明</h3>
        <ol>
          <li>上传一张或多张图片文件（支持JPG、PNG、WEBP、GIF、BMP格式）</li>
          <li>上传音频文件作为背景音乐（视频时长将匹配音频时长）</li>
          <li>
            设置视频参数：
            <ul>
              <li>
                <strong>每张图片显示时长：</strong>单张图片在视频中的显示时间
              </li>
              <li><strong>过渡效果时长：</strong>图片切换时的过渡动画时间</li>
              <li><strong>视频尺寸：</strong>输出视频的分辨率</li>
            </ul>
          </li>
          <li>选择过渡效果：无、淡入淡出、滑动、缩放（仅多张图片时有效）</li>
          <li>选择滤镜效果：无、模糊、锐化、复古、黑白、怀旧</li>
          <li>点击"开始生成视频"按钮，等待处理完成</li>
          <li>处理完成后可以预览结果并下载</li>
        </ol>
        <p>
          💡 提示：
          <br />• 如果图片总时长小于音频时长，图片将循环播放直到音频结束 <br />•
          如果图片总时长大于音频时长，视频将在音频结束时停止 <br />•
          所有处理都在您的浏览器中完成，文件不会上传到服务器，保证隐私安全
        </p>
      </div>

      <footer>
        <p>🔒 本工具完全在浏览器本地运行，保护您的隐私安全</p>
        <p>⚡ 基于 FFmpeg.wasm 技术，无需安装任何软件</p>
      </footer>
    </div>

    <script>
      // 全局变量
      let ffmpeg = null
      let imageFiles = []
      let audioFile = null
      let isProcessing = false

      // DOM 元素
      const imageUpload = document.getElementById('imageUpload')
      const audioUpload = document.getElementById('audioUpload')
      const imageInput = document.getElementById('imageInput')
      const audioInput = document.getElementById('audioInput')
      const imagePreview = document.getElementById('imagePreview')
      const audioPreview = document.getElementById('audioPreview')
      const imageGrid = document.getElementById('imageGrid')
      const audioElement = document.getElementById('audioElement')
      const imageInfo = document.getElementById('imageInfo')
      const audioInfo = document.getElementById('audioInfo')
      const processBtn = document.getElementById('processBtn')
      const progressContainer = document.getElementById('progressContainer')
      const progressFill = document.getElementById('progressFill')
      const progressText = document.getElementById('progressText')
      const statusMessage = document.getElementById('statusMessage')
      const errorMessage = document.getElementById('errorMessage')
      const resultSection = document.getElementById('resultSection')
      const resultVideo = document.getElementById('resultVideo')
      const downloadBtn = document.getElementById('downloadBtn')
      const resetBtn = document.getElementById('resetBtn')

      // 初始化 FFmpeg
      async function initFFmpeg() {
        if (!ffmpeg) {
          ffmpeg = FFmpeg.createFFmpeg({
            log: true,
          })

          ffmpeg.setLogger(({ message }) => {
            const timeMatch = message.match(/time=(\d{2}:\d{2}:\d{2}\.\d{2})/)
            if (timeMatch) {
              const timeStr = timeMatch[1]
              updateProgress(50, `处理中... time=${timeStr} (参考时间)`)
            }
          })

          ffmpeg.setProgress(({ ratio }) => {
            const percent = Math.round(ratio * 100)
            progressFill.style.width = `${percent}%`
          })
        }

        if (!ffmpeg.isLoaded()) {
          showStatus('正在加载 FFmpeg...')
          await ffmpeg.load()
          showStatus('FFmpeg 加载完成')
        }
      }

      // 文件上传处理
      function setupFileUpload(uploadArea, input, fileType) {
        // 点击上传
        uploadArea.addEventListener('click', (e) => {
          if (e.target === input) return
          e.preventDefault()
          input.click()
        })

        // 拖拽上传
        uploadArea.addEventListener('dragover', (e) => {
          e.preventDefault()
          uploadArea.classList.add('dragover')
        })

        uploadArea.addEventListener('dragleave', () => {
          uploadArea.classList.remove('dragover')
        })

        uploadArea.addEventListener('drop', (e) => {
          e.preventDefault()
          uploadArea.classList.remove('dragover')
          const files = e.dataTransfer.files
          if (files.length > 0) {
            if (fileType === 'image') {
              handleImageFiles(Array.from(files))
            } else {
              handleAudioFile(files[0])
            }
          }
        })

        // 文件选择
        input.addEventListener('change', (e) => {
          if (e.target.files.length > 0) {
            if (fileType === 'image') {
              handleImageFiles(Array.from(e.target.files))
            } else {
              handleAudioFile(e.target.files[0])
            }
            e.target.value = ''
          }
        })
      }

      // 处理图片文件
      function handleImageFiles(files) {
        // 过滤出图片文件
        const validImages = files.filter((file) =>
          file.type.startsWith('image/')
        )

        if (validImages.length === 0) {
          showStatus('请选择有效的图片文件', true)
          return
        }

        imageFiles = [...imageFiles, ...validImages]
        updateImagePreview()
        checkCanProcess()
        showStatus(
          `已添加 ${validImages.length} 张图片，总计 ${imageFiles.length} 张`
        )
      }

      // 处理音频文件
      function handleAudioFile(file) {
        if (!file.type.startsWith('audio/')) {
          showStatus('请选择有效的音频文件', true)
          return
        }

        audioFile = file
        const url = URL.createObjectURL(file)
        audioElement.src = url
        audioPreview.style.display = 'block'

        const fileSize = (file.size / (1024 * 1024)).toFixed(2)
        audioInfo.innerHTML = `
          <strong>文件名:</strong> ${file.name}<br>
          <strong>大小:</strong> ${fileSize} MB<br>
          <strong>类型:</strong> ${file.type}
        `

        checkCanProcess()
        showStatus('音频文件上传成功')
      }

      // 更新图片预览
      function updateImagePreview() {
        imageGrid.innerHTML = ''

        if (imageFiles.length === 0) {
          imagePreview.style.display = 'none'
          return
        }

        imagePreview.style.display = 'block'

        imageFiles.forEach((file, index) => {
          const imageItem = document.createElement('div')
          imageItem.className = 'image-item'

          const img = document.createElement('img')
          img.src = URL.createObjectURL(file)
          img.alt = file.name

          const removeBtn = document.createElement('button')
          removeBtn.className = 'remove-btn'
          removeBtn.innerHTML = '×'
          removeBtn.onclick = () => removeImage(index)

          const orderNumber = document.createElement('div')
          orderNumber.className = 'order-number'
          orderNumber.textContent = index + 1

          imageItem.appendChild(img)
          imageItem.appendChild(removeBtn)
          imageItem.appendChild(orderNumber)
          imageGrid.appendChild(imageItem)
        })

        const totalSize = imageFiles.reduce((sum, file) => sum + file.size, 0)
        const totalSizeMB = (totalSize / (1024 * 1024)).toFixed(2)

        imageInfo.innerHTML = `
          <strong>图片数量:</strong> ${imageFiles.length} 张<br>
          <strong>总大小:</strong> ${totalSizeMB} MB
        `
      }

      // 移除图片
      function removeImage(index) {
        imageFiles.splice(index, 1)
        updateImagePreview()
        checkCanProcess()
        showStatus(`已移除图片，剩余 ${imageFiles.length} 张`)
      }

      // 检查是否可以开始处理
      function checkCanProcess() {
        processBtn.disabled =
          !(imageFiles.length > 0 && audioFile) || isProcessing
      }

      // 显示状态消息
      function showStatus(message, isError = false) {
        if (isError) {
          errorMessage.textContent = message
          statusMessage.textContent = ''
        } else {
          statusMessage.textContent = message
          errorMessage.textContent = ''
        }
      }

      // 更新进度
      function updateProgress(percent, text) {
        progressFill.style.width = `${percent}%`
        progressText.textContent = text
      }

      // 显示进度条
      function showProgress() {
        progressContainer.style.display = 'block'
        statusMessage.classList.add('processing')
      }

      // 隐藏进度条
      function hideProgress() {
        progressContainer.style.display = 'none'
        statusMessage.classList.remove('processing')
      }

      // 获取滤镜参数
      function getFilterString() {
        const filter = document.querySelector(
          'input[name="filter"]:checked'
        ).value

        switch (filter) {
          case 'blur':
            return 'boxblur=2:1'
          case 'sharpen':
            return 'unsharp=5:5:1.0:5:5:0.0'
          case 'sepia':
            return 'colorchannelmixer=.393:.769:.189:0:.349:.686:.168:0:.272:.534:.131'
          case 'grayscale':
            return 'colorchannelmixer=.3:.4:.3:0:.3:.4:.3:0:.3:.4:.3'
          case 'vintage':
            return 'curves=vintage'
          default:
            return null
        }
      }

      // 获取过渡效果参数
      function getTransitionFilter(
        transition,
        duration,
        imageDuration,
        isMultiImage = false
      ) {
        if (!isMultiImage) {
          // 单张图片模式的过渡效果
          switch (transition) {
            case 'fade':
              return `fade=t=in:st=0:d=${duration},fade=t=out:st=${
                imageDuration - duration
              }:d=${duration}`
            case 'zoom':
              return `zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':d=${
                imageDuration * 25
              }:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)'`
            default:
              return null
          }
        } else {
          // 多张图片模式的过渡效果
          switch (transition) {
            case 'fade':
              // 为每张图片添加淡入淡出效果
              return `fade=t=in:st=0:d=${duration},fade=t=out:st=${
                imageDuration - duration
              }:d=${duration}`
            case 'zoom':
              return `zoompan=z='if(lte(zoom,1.0),1.2,max(1.001,zoom-0.001))':d=${
                imageDuration * 25
              }:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)'`
            default:
              return null
          }
        }
      }

      // 处理图片转视频
      async function processImageToVideo() {
        if (imageFiles.length === 0 || !audioFile || isProcessing) return

        isProcessing = true
        processBtn.disabled = true
        showProgress()

        try {
          await initFFmpeg()

          // 获取设置参数
          const imageDuration =
            parseFloat(document.getElementById('imageDuration').value) || 2
          const transitionDuration =
            parseFloat(document.getElementById('transitionDuration').value) ||
            0.5
          const videoSize = document.getElementById('videoSize').value
          const transition = document.querySelector(
            'input[name="transition"]:checked'
          ).value
          const filter = getFilterString()

          showStatus('正在准备文件...')
          updateProgress(10, '准备文件中...')

          // 写入音频文件
          const audioName = 'input_audio.' + audioFile.name.split('.').pop()
          const audioData = new Uint8Array(await audioFile.arrayBuffer())
          ffmpeg.FS('writeFile', audioName, audioData)

          // 写入图片文件
          const imageNames = []
          for (let i = 0; i < imageFiles.length; i++) {
            const imageName = `image_${i
              .toString()
              .padStart(3, '0')}.${imageFiles[i].name.split('.').pop()}`
            const imageData = new Uint8Array(await imageFiles[i].arrayBuffer())
            ffmpeg.FS('writeFile', imageName, imageData)
            imageNames.push(imageName)
          }

          showStatus('开始生成视频...')
          updateProgress(30, '生成视频中...')

          const outputName = 'output_video.mp4'
          const [width, height] = videoSize.split('x')

          let ffmpegArgs = []

          if (imageFiles.length === 1) {
            // 单张图片模式 - 循环播放直到音频结束
            let videoFilter = `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`

            // 添加过渡效果
            const transitionFilter = getTransitionFilter(
              transition,
              transitionDuration,
              imageDuration,
              false
            )
            if (transitionFilter) {
              videoFilter += ',' + transitionFilter
            }

            // 添加滤镜效果
            if (filter) {
              videoFilter += ',' + filter
            }

            ffmpegArgs = [
              '-loop',
              '1',
              '-i',
              imageNames[0],
              '-i',
              audioName,
              '-c:v',
              'libx264',
              '-c:a',
              'aac',
              '-b:a',
              '128k',
              '-vf',
              videoFilter,
              '-pix_fmt',
              'yuv420p',
              '-r',
              '25',
              '-preset',
              'ultrafast',
              '-crf',
              '28',
              '-movflags',
              '+faststart',
              '-map',
              '0:v:0',
              '-map',
              '1:a:0',
              '-y',
              outputName,
            ]
          } else {
            // 多张图片模式 - 使用更简单的方法避免内存问题
            // 创建基础的图片列表文件（只包含一轮循环）
            let concatContent = ''

            for (let i = 0; i < imageNames.length; i++) {
              concatContent += `file '${imageNames[i]}'\n`
              concatContent += `duration ${imageDuration}\n`
            }
            // 最后一张图片需要重复一次（ffmpeg concat 要求）
            concatContent += `file '${imageNames[imageNames.length - 1]}'\n`

            ffmpeg.FS('writeFile', 'images.txt', concatContent)

            // 创建视频滤镜
            let videoFilter = `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`

            // 添加过渡效果
            const transitionFilter = getTransitionFilter(
              transition,
              transitionDuration,
              imageDuration,
              true
            )
            if (transitionFilter) {
              videoFilter += ',' + transitionFilter
            }

            // 添加滤镜效果
            if (filter) {
              videoFilter += ',' + filter
            }

            // 添加循环滤镜，让视频内容循环播放
            videoFilter += ',loop=loop=-1:size=32767:start=0'

            ffmpegArgs = [
              '-f',
              'concat',
              '-safe',
              '0',
              '-i',
              'images.txt',
              '-i',
              audioName,
              '-c:v',
              'libx264',
              '-c:a',
              'aac',
              '-b:a',
              '128k',
              '-vf',
              videoFilter,
              '-pix_fmt',
              'yuv420p',
              '-r',
              '25',
              '-preset',
              'ultrafast',
              '-crf',
              '28',
              '-movflags',
              '+faststart',
              '-shortest',
              '-map',
              '0:v:0',
              '-map',
              '1:a:0',
              '-y',
              outputName,
            ]
          }

          // 执行 FFmpeg 命令
          await ffmpeg.run(...ffmpegArgs)

          showStatus('正在生成结果...')
          updateProgress(90, '生成结果中...')

          // 读取输出文件
          const outputData = ffmpeg.FS('readFile', outputName)
          const outputBlob = new Blob([outputData.buffer], {
            type: 'video/mp4',
          })
          const outputUrl = URL.createObjectURL(outputBlob)

          // 显示结果
          resultVideo.src = outputUrl
          resultSection.style.display = 'block'

          // 设置下载
          downloadBtn.onclick = () => {
            const a = document.createElement('a')
            a.href = outputUrl
            a.download = `image_to_video_${Date.now()}.mp4`
            a.click()
          }

          showStatus('视频生成完成！')
          updateProgress(100, '完成')
          hideProgress()

          // 清理文件系统
          ffmpeg.FS('unlink', audioName)
          imageNames.forEach((name) => {
            ffmpeg.FS('unlink', name)
          })
          if (imageFiles.length > 1) {
            ffmpeg.FS('unlink', 'images.txt')
          }
          ffmpeg.FS('unlink', outputName)
        } catch (error) {
          console.error('处理失败:', error)
          showStatus('处理失败: ' + error.message, true)
          hideProgress()
        } finally {
          isProcessing = false
          checkCanProcess()
        }
      }

      // 重置所有状态
      function resetAll() {
        imageFiles = []
        audioFile = null
        isProcessing = false

        imageInput.value = ''
        audioInput.value = ''
        imagePreview.style.display = 'none'
        audioPreview.style.display = 'none'
        resultSection.style.display = 'none'

        hideProgress()
        showStatus('')
        checkCanProcess()
      }

      // 初始化事件监听器
      function initEventListeners() {
        setupFileUpload(imageUpload, imageInput, 'image')
        setupFileUpload(audioUpload, audioInput, 'audio')

        processBtn.addEventListener('click', processImageToVideo)
        resetBtn.addEventListener('click', resetAll)
      }

      // 页面加载完成后初始化
      document.addEventListener('DOMContentLoaded', () => {
        initEventListeners()
      })
    </script>
  </body>
</html>
